package wiki.xsx.core.snowflake.config;

import java.io.Serializable;

/**
 * 雪花
 * @author xsx
 * @date 2019/11/19
 * @since 1.8
 */
public final class Snowflake implements Serializable {

    private static final long serialVersionUID = -2282650019490872043L;

    /**
     * 当前机器码id
     */
    private final long workerId;
    /**
     * 当前数据中心id
     */
    private final long dataCenterId;
    /**
     * 上一次时间戳
     */
    private long lastTimestamp = -1L;
    /**
     * 起始时间（2000-01-01 00：00：00）
     */
    private final long epoch = 946656000000L;
    /**
     * 序列
     */
    private long sequence = 0L;
    /**
     * 序列长度
     */
    private final long sequenceLength;
    /**
     * 序列最大值
     */
    private final long sequenceMax;

    /**
     * 构造
     * @param workerId 终端ID
     * @param dataCenterId 数据中心ID
     */
    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, SnowflakeMode.NORMAL);
    }

    /**
     * 构造
     * @param workerId 机器码id
     * @param dataCenterId 数据中心id
     * @param mode 雪花模式
     */
    public Snowflake(long workerId, long dataCenterId, SnowflakeMode mode) {
        // 最大支持机器节点数0~31，一共32个
        long maxWorkerId = 31L;
        // 如果机器码id大于最大id或小于0，则提示错误信息
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %s or less than 0", maxWorkerId));
        }
        // 最大支持数据中心节点数0~127，一共128个
        long maxDatacenterId = SnowflakeMode.MAX.getCount();
        // 如果雪花模式最大支持数量大于最大数据中心id，则提示错误信息
        if (mode.getCount() > maxDatacenterId) {
            throw new IllegalArgumentException(String.format("max datacenter Id can't be greater than %s", maxDatacenterId));
        }
        // 如果数据中心id大于雪花模式最大支持数量或数据中心id小于0，则提示错误信息
        if (dataCenterId > mode.getCount() || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %s or less than 0", mode.getCount()));
        }
        // 初始化机器码id
        this.workerId = workerId;
        // 初始化数据中心id
        this.dataCenterId = dataCenterId;
        // 总长度，64位减1
        long totalLength = 64 - 1;
        // 时间长度
        long timeLength = 41;
        // 机器长度
        long workerLength = 5;
        // 数据长度
        long dataLength = Long.toBinaryString(mode.getCount()).length();
        // 序列长度
        this.sequenceLength = totalLength - timeLength - workerLength - dataLength;
        // 序列最大值
        this.sequenceMax = ~(-1L<<this.sequenceLength);
    }

    /**
     * 获取机器id
     * @return 所属机器的id
     */
    public long getWorkerId() {
        return this.workerId;
    }

    /**
     * 获取数据中心id
     * @return 所属数据中心id
     */
    public long getDataCenterId() {
        return this.dataCenterId;
    }

    /**
     * 下一个ID
     * @return ID
     */
    public synchronized long nextId() {
        // 获取当前时间戳
        long timestamp = System.currentTimeMillis();
        // 如果当前时间戳小于上一次时间戳，则提示错误信息
        if (timestamp < this.lastTimestamp) {
            // 如果服务器时间有问题(时钟后退) 报错。
            throw new IllegalStateException(String.format("Clock moved backwards. Refusing to generate id for %sms", this.lastTimestamp - timestamp));
        }
        // 如果上一次时间戳等于当前时间戳，则重置序列
        if (this.lastTimestamp == timestamp) {
            // 重置序列
            this.sequence = (this.sequence + 1) & this.sequenceMax;
            // 如果序列等于0，则重置当前时间戳
            if (this.sequence == 0) {
                // 重置当前时间戳
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        }
        // 否则重置序列为0
        else {
            // 重置序列为0
            this.sequence = 0L;
        }
        // 重置上一次时间戳为当前时间戳
        this.lastTimestamp = timestamp;
        // 返回id
        return ((timestamp - this.epoch) << 22L) | (this.dataCenterId << 17L) | (this.workerId << this.sequenceLength) | this.sequence;
    }

    /**
     * 下一个ID（字符串形式）
     * @return ID 字符串形式
     */
    public String nextIdStr() {
        return Long.toString(nextId());
    }

    /**
     * 解析id获取时间戳
     * @param id id
     * @return 返回时间戳
     */
    public long getTimestamp(long id) {
        return (id >> 22L & ~(-1L << 41L)) + this.epoch;
    }

    // ------------------------------------------------------------------------------------------------------------------------------------ Private method start
    /**
     * 循环等待下一个时间
     * @param lastTimestamp 上次记录的时间
     * @return 下一个时间
     */
    private long tilNextMillis(long lastTimestamp) {
        // 获取当前时间戳
        long timestamp = System.currentTimeMillis();
        // 如果当前时间戳小于等于上一次时间戳，则重置当前时间戳
        while (timestamp <= lastTimestamp) {
            // 重置当前时间戳
            timestamp = System.currentTimeMillis();
        }
        // 返回当前时间戳
        return timestamp;
    }
    // ------------------------------------------------------------------------------------------------------------------------------------ Private method end
}
